{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit PluginManager;
{$WARN SYMBOL_PLATFORM OFF}

interface

uses Classes, UIObjects, RenderCache, Embedding, AHMTypes, Mediate_TLB,
     MediateServer, XMLHelper, AHMConsts;

const
  // Default values for plugin wrapper
  DEF_PLUGIN_TYPE = ptNative;
  DEF_PATH = '';
  DEF_PARAMS = '';
  DEF_WIN_CLASS = '';
  DEF_WIN_NAME = '';
  DEF_WIN_LOCK = False;
  DEF_VERSION = 'No version';
  DEF_AUTHOR = 'No author';
  DEF_DOCS = 'No documentation';

  NORMAL_UPDATE_TIME = 200; // Render refresh delay when using plugin
  IDLE_UPDATE_TIME = 800; // Render refresh delay when plugin is idle

{$IFDEF DESIGNER}
  BROWSE_FILE_TITLE = 'Select plugin file';
  COPY_FILE_CONFIRM = 'Would you like to copy this file into the mediate ' +
                      'plugins directory?';

  // Property categories for designer
  CAT_GENERAL = 'General';
  CAT_PLUGIN = 'Plugin';

  // Property hints for designer
  HINT_NAME = 'defines a unique display name for the plugin';
  HINT_DESCRIPTION = 'defines a description for the plugin';
  HINT_COMMENT = 'provides information for users of the plugin';
  HINT_VERSION = 'specifies the version number for the plugin';
  HINT_AUTHOR = 'specifies the author of the plugin';
  HINT_PLUGINTYPE = 'specifies the type of plugin (native DLL or external application)';
  HINT_INTERNALNAME = 'defines an internal name for the plugin (to be used within themes)';
  HINT_PATH = 'specifies the path to a native plugin DLL or external executable';
  HINT_PARAMS = 'specifies any parameters to pass when loading external executable';
  HINT_WINDOWNAME = 'specifies the window name of the external executable (supports ? and * wildcards)';
  HINT_WINDOWCLASS = 'specifies the window class of the external executable (supports ? and * wildcards)';
  HINT_WINDOWLOCK = 'controls whether the external executable window is locked whilst rendering';
{$ENDIF}

type
  TAHMUpdatePriority = (upImmediate, upNormal, upIdle);

  TAHMPlugin = class;

  TAHMPluginWrapper = class(TAHMUIObject)
  private
    FParsed: Boolean;
    FValid: Boolean;
    FEnabled: Boolean;
    FFilename: String;
    FInternalName: String;
    FPath: TAHMFilename;
    FParams: String;
    FWindowName: String;
    FWindowClass: String;
    FWindowLock: Boolean;
    FPluginType: TAHMPluginType;
    FVersion: WideString;
    FAuthor: WideString;
    FPlugin: TAHMPlugin;
  protected
    function GetLoaded: Boolean;
    function GetStarted: Boolean;
    function GetConfigurable: Boolean;
    function CleanInternalName(DirtyName: String): String;
    procedure SetFilename(Filename: String);
    procedure SetInternalName(InternalName: String);
    procedure SetPluginType(PluginType: TAHMPluginType);
    procedure SetPath(Path: TAHMFilename);
    procedure SetParameters(Parameters: String);
    procedure SetWindowName(WindowName: String);
    procedure SetWindowClass(WindowClass: String);
    procedure SetWindowLock(WindowLock: Boolean);
    procedure ClearProperties; override;
  public
    constructor Create(const Name: String); override;
    destructor Destroy; override;
    property Filename: String read FFilename write SetFileName;
    property Loaded: Boolean read GetLoaded;
    property Started: Boolean read GetStarted;
    property Configurable: Boolean read GetConfigurable;
    property Enabled: Boolean read FEnabled write FEnabled;
    property Valid: Boolean read FValid;
    function RenderPreview: Boolean; override;
    function Modify(Action: Integer): Boolean; override;
    procedure Assign(Source: TPersistent); override;
    procedure SaveAsXML(Lines: TStrings); override;
    procedure ParseFromXML(XML: TAHMXMLHelper); override;
    procedure BuildProperties(Properties: TAHMUIPropertyList); override;
    procedure ParsePluginProperties(XML: TAHMXMLHelper);
    function ParseDescriptor: Boolean;
    procedure StoreDescriptor;
    function Load: Boolean;
    procedure Initialise;
    procedure Uninitialise;
    procedure Hibernate;
    procedure Resume;
    function Unload: Boolean;
    procedure Reload;
    function Start: Boolean;
    function Stop: Boolean;
    function Update: Boolean;
    function Show(ControlName: String): Boolean;
    function Hide(ControlName: String): Boolean;
    procedure HideFromAllControls;
    function Configure: Boolean;
    procedure Subscribe(ActionName: String);
    procedure Unsubscribe(ActionName: String);
    procedure ProcessAction(ActionName: String);
    procedure EmbedApplication(WindowName, WindowClass: String; WindowLock: Boolean);
    procedure LaunchApplication(ExecutablePath, Parameters: String);
    procedure CloseApplication(ExecutablePath: String);
    procedure CloseAllApplications;
    class function ClassName: ShortString; override;
  published
    property InternalName: String read FInternalName write SetInternalName;
    property PluginType: TAHMPluginType read FPluginType write SetPluginType;
    property Path: TAHMFilename read FPath write SetPath;
    property Parameters: String read FParams write SetParameters;
    property WindowName: String read FWindowName write SetWindowName;
    property WindowClass: String read FWindowClass write SetWindowClass;
    property WindowLock: Boolean read FWindowLock write SetWindowLock;
    property Version: WideString read FVersion write FVersion;
    property Author: WideString read FAuthor write FAuthor;
  end;

  TAHMPlugin = class
  private
    FOwner: TAHMPluginWrapper;
    FEmbed: TAHMEmbeddedInstance;
    FSubscriptions: TStringList;
    FApplications: TStringList;
    FUpdatePriority: TAHMUpdatePriority;
    FRenderRequired: Boolean;
    FRenderTimestamp: TAHMTimestamp;
    FRenderCache: TAHMRenderCache;
    FStarted: Boolean;
  protected
    function GetLoaded: Boolean; virtual;
    function GetConfigurable: Boolean; virtual;
    function IsSubscribed(var ActionName: String): Boolean;
  public
    constructor Create(Owner: TAHMPluginWrapper);
    destructor Destroy; override;
    property Loaded: Boolean read GetLoaded;
    property Started: Boolean read FStarted;
    property Configurable: Boolean read GetConfigurable;
    property RenderRequired: Boolean read FRenderRequired;
    property RenderCache: TAHMRenderCache read FRenderCache;
    function Load: Boolean; virtual;
    procedure Initialise; virtual;
    procedure Uninitialise; virtual;
    procedure Hibernate; virtual;
    procedure Resume; virtual;
    function Unload: Boolean; virtual;
    function Start: Boolean; virtual;
    function Stop: Boolean; virtual;
    function Update: Boolean; virtual;
    function Render: Boolean; virtual;
    function Configure: Boolean; virtual;
    procedure Subscribe(ActionName: String); virtual;
    procedure Unsubscribe(ActionName: String); virtual;
    procedure ProcessAction(ActionName: String); virtual;
    procedure EmbedApplication(WindowName, WindowClass: String; WindowLock: Boolean); virtual;
    procedure LaunchApplication(ExecutablePath, Parameters: String); virtual;
    procedure CloseApplication(ExecutablePath: String); virtual;
    procedure CloseAllApplications; virtual;
    procedure MouseDown(X, Y: Single); virtual;
    procedure MouseUp(X, Y: Single); virtual;
    procedure MouseMove(X, Y: Single; Down: Boolean); virtual;
    procedure KeyPress(KeyCode: String); overload; virtual;
    procedure KeyPress(KeyCode: Word; Shift: TShiftState); overload; virtual;
  end;

  TAHMNativePlugin = class(TAHMPlugin)
  private
    FDLLHandle: Integer;
    FPlugin: IMediatePlugin;
    FServer: TMediateServer;
    FRenderImplemented: Boolean;
  protected
    function GetLoaded: Boolean; override;
    function GetConfigurable: Boolean; override;
    procedure LoadPluginProperties;
  public
    function Load: Boolean; override;
    procedure Initialise; override;
    procedure Uninitialise; override;
    procedure Hibernate; override;
    procedure Resume; override;
    function Configure: Boolean; override;
    function Unload: Boolean; override;
    function Start: Boolean; override;
    function Stop: Boolean; override;
    function Update: Boolean; override;
    function Render: Boolean; override;
    procedure ProcessAction(ActionName: String); override;
  end;

  TAHMExternalPlugin = class(TAHMPlugin)
  private
    FLoaded: Boolean;
  protected
    function GetLoaded: Boolean; override;
    function GetConfigurable: Boolean; override;
  public
    function Load: Boolean; override;
    function Configure: Boolean; override;
    function Unload: Boolean; override;
    function Start: Boolean; override;
    function Stop: Boolean; override;
    function Render: Boolean; override;
  end;

  TAHMPlugins = class
  private
    FPlugins: TList;
  protected
    function GetCount: Integer;
    function GetPlugin(Index: Integer): TAHMPluginWrapper;
    procedure Clear;
    function AddPlugin(Filename: String): TAHMPluginWrapper;
  public
    constructor Create;
    destructor Destroy; override;
    property Count: Integer read GetCount;
    property Plugin[Index: Integer]: TAHMPluginWrapper read GetPlugin; default;
    function FindPlugin(InternalName: String): TAHMPluginWrapper;
  end;

  TAHMPluginManager = class
  private
    FPlugins: TAHMPlugins;
    FEmbedManager: TAHMEmbeddingManager;
  public
    constructor Create;
    destructor Destroy; override;
    property Plugins: TAHMPlugins read FPlugins;
    property EmbeddingManager: TAHMEmbeddingManager read FEmbedManager;
    procedure LoadPluginDescriptors;
    procedure SavePluginDescriptors;
    function InspectPlugin(PluginName: String): TAHMPluginWrapper;
    function LoadPlugin(PluginName: String): Boolean;
    function UnloadPlugin(PluginName: String): Boolean;
    function StartPlugin(PluginName: String): Boolean;
    function StopPlugin(PluginName: String): Boolean;
    function UpdatePlugin(PluginName: String): Boolean;
    function ShowPlugin(PluginName, ControlName: String): Boolean;
    function HidePlugin(PluginName, ControlName: String): Boolean;
    function SubscribePlugin(PluginName, ActionName: String): Boolean;
    function UnSubscribePlugin(PluginName, ActionName: String): Boolean;
    function EmbedPluginApplication(PluginName, WindowName, WindowClass: String; WindowLock: Boolean): Boolean;
    function LaunchPluginApplication(PluginName, ExecutablePath, Parameters: String): Boolean;
    function ClosePluginApplication(PluginName, ExecutablePath: String): Boolean;
    function CloseAllPluginApplications(PluginName: String): Boolean;
    function BrowsePluginFile(var Filename: String; PluginType: TAHMPluginType): Boolean;
    procedure ProcessAction(ActionName: String);
    procedure HibernatePlugins;
    procedure ResumePlugins;
    procedure UnloadPlugins;
  end;

implementation

uses Windows, ActiveX, ComObj, SysUtils, ApplicationManager, Logging,
     {$IFDEF DESIGNER}Dialogs, Controls, JclFileUtils,{$ENDIF}
     InterfaceDescriptor, BaseControls, UIControls, Timing,
     Utilities, Screen, TagNames, XMLTags, ErrorCodes;

// TAHMPluginWrapper

constructor TAHMPluginWrapper.Create(const Name: String);
begin
  inherited Create(Name);

  ObjectType := otPlugins;
  ImageIndex := Ord(otPluginWindows);

  ClearProperties;
end;

destructor TAHMPluginWrapper.Destroy;
begin
  Unload;

  inherited;
end;

function TAHMPluginWrapper.GetLoaded: Boolean;
begin
  Result := Assigned(FPlugin) and FPlugin.Loaded;
end;

function TAHMPluginWrapper.GetStarted: Boolean;
begin
  Result := Assigned(FPlugin) and FPlugin.Started;
end;

function TAHMPluginWrapper.GetConfigurable: Boolean;
begin
  Result := Assigned(FPlugin) and FPlugin.Configurable;
end;

function TAHMPluginWrapper.CleanInternalName(DirtyName: String): String;
begin
  // Remove any spaces and file extension from internal plugin name
  Result := StringReplace(DirtyName, ' ', '', [rfReplaceAll]);
  Result := UpperCase(ChangeFileExt(Result, ''));
end;

procedure TAHMPluginWrapper.SetFilename(Filename: String);
begin
  // Populate filename and derive internal plugin name
  FFilename := Filename;
  FInternalName := CleanInternalName(Filename);
end;

procedure TAHMPluginWrapper.SetInternalName(InternalName: String);
begin
  // Change filename to match new internal name
  FInternalName := CleanInternalName(InternalName);
  FFilename := LowerCase(FInternalName + EXT_PLUGIN);
end;

procedure TAHMPluginWrapper.SetPluginType(PluginType: TAHMPluginType);
begin
  if FPluginType <> PluginType then
  begin
    FPluginType := PluginType;

    // Clear path; this must be wrong if type changes
    FPath := '';

    // Clear redundant properties for this plugin type
    if PluginType = ptNative then
    begin
      FParams := '';
      FWindowName := '';
      FWindowClass := '';
      FWindowLock := False;
    end;
  end;
end;

procedure TAHMPluginWrapper.SetPath(Path: TAHMFilename);
begin
  if FPath <> Path then
  begin
    // Check extension to determine plugin type
    if UpperCase(ExtractFileExt(Path)) = EXT_NATIVE then PluginType := ptNative
                                                    else PluginType := ptExternal;
    FPath := Path;
  end;
end;

procedure TAHMPluginWrapper.SetParameters(Parameters: String);
begin
  if FParams <> Parameters then
  begin
    PluginType := ptExternal;
    FParams := Parameters;
  end;
end;

procedure TAHMPluginWrapper.SetWindowName(WindowName: String);
begin
  if FWindowName <> WindowName then
  begin
    PluginType := ptExternal;
    FWindowName := WindowName;
  end;
end;

procedure TAHMPluginWrapper.SetWindowClass(WindowClass: String);
begin
  if FWindowClass <> WindowClass then
  begin
    PluginType := ptExternal;
    FWindowClass := WindowClass;
  end;
end;

procedure TAHMPluginWrapper.SetWindowLock(WindowLock: Boolean);
begin
  if FWindowLock <> WindowLock then
  begin
    PluginType := ptExternal;
    FWindowLock := WindowLock;
  end;
end;

procedure TAHMPluginWrapper.ClearProperties;
begin
  inherited;

  // Clear plugin properties
  FPluginType := DEF_PLUGIN_TYPE;
  FPath := DEF_PATH;
  FParams := DEF_PARAMS;
  FWindowName := DEF_WIN_NAME;
  FWindowClass := DEF_WIN_CLASS;
  FWindowLock := DEF_WIN_LOCK;
  FVersion := DEF_VERSION;
  FAuthor := DEF_AUTHOR;
end;

function TAHMPluginWrapper.RenderPreview: Boolean;
begin
{$IFDEF DESIGNER}
  with ScreenInstance.Viewport do
  begin
    Canvas.DesignerBackground(ViewportRect);
    Canvas.TextRect(ViewportRect, nil, 'Plugin Properties' + sLineBreak +
                    sLineBreak + 'Name: ' + Name +
                    sLineBreak + 'Description: ' + Description +
                    sLineBreak + 'Internal Name: ' + FInternalName +
                    sLineBreak + 'Version: ' + FVersion +
                    sLineBreak + 'Author: ' + FAuthor +
                    sLineBreak + 'Documentation: ' + sLineBreak + Comment);
  end;
{$ENDIF}
  Result := True;
end;

function TAHMPluginWrapper.Modify(Action: Integer): Boolean;
var
  NewFile: String;
begin
  // Use plugin manager to select and copy plugin file
  NewFile := FPath;
  Result := ApplicationManagerInstance.PluginManager.BrowsePluginFile(NewFile, FPluginType);
  if Result then SetPath(NewFile);
  if Result then DoModified;
end;

procedure TAHMPluginWrapper.Assign(Source: TPersistent);
begin
  inherited;

  with Source as TAHMPluginWrapper do
  begin
    Self.FVersion := Version;
    Self.FAuthor := Author;
    Self.FPluginType := PluginType;
    Self.FPath := Path;
    Self.FParams := Parameters;
    Self.FWindowName := WindowName;
    Self.FWindowClass := WindowClass;
    Self.FWindowLock := WindowLock;
  end;
end;

procedure TAHMPluginWrapper.SaveAsXML(Lines: TStrings);
var
  sWork: String;
begin
  // Build root tag with attributes
  sWork := TGB+TAG_PLUGIN_DESCRIPTOR;
  if Name <> '' then sWork := sWork + SPC+ATTR_NAME+ATB + EncodeXML(Name) + ATQ;
  if Description <> '' then sWork := sWork + SPC+ATTR_DESC+ATB + EncodeXML(Description) + ATQ;
  Lines.Add(sWork + TGC);

  // Build plugin type tag
  sWork := TGB+TAG_PLUGIN;
  if FPluginType = ptNative then
    sWork := sWork + SPC+ATTR_TYPE+ATB+ATTR_VAL_NATIVE+ATC
  else if FPluginType = ptExternal then
    sWork := sWork + SPC+ATTR_TYPE+ATB+ATTR_VAL_EXTERNAL+ATC
  else
    sWork := sWork + TGC;
  Lines.Add(TABS[1] + sWork);

  // Add common tags & custom tags for external plugins
  Lines.Add(TABS[2] + BeginTag(TAG_PATH) + EncodeXML(FPath) + EndTag(TAG_PATH));
  if (FPluginType = ptExternal) and (FParams <> '') then
    Lines.Add(TABS[2] + BeginTag(TAG_PARAMS) + EncodeXML(FParams) + EndTag(TAG_PARAMS));
  if (FPluginType = ptExternal) and (FWindowClass <> '') then
    Lines.Add(TABS[2] + BeginTag(TAG_WIN_CLASS) + EncodeXML(FWindowClass) + EndTag(TAG_WIN_CLASS));
  if (FPluginType = ptExternal) and (FWindowName <> '') then
    Lines.Add(TABS[2] + BeginTag(TAG_WIN_NAME) + EncodeXML(FWindowName) + EndTag(TAG_WIN_NAME));
  if (FPluginType = ptExternal) then
    Lines.Add(TABS[2] + BeginTag(TAG_WIN_LOCK) + Bool2Str(FWindowLock) + EndTag(TAG_WIN_LOCK));
  Lines.Add(TABS[1] + EndTag(TAG_PLUGIN));

  // Write remaining plugin descriptor properties to XML file
  Lines.Add(TABS[1] + BeginTag(TAG_VERSION) + EncodeXML(FVersion) + EndTag(TAG_VERSION));
  Lines.Add(TABS[1] + BeginTag(TAG_AUTHOR) + EncodeXML(FAuthor) + EndTag(TAG_AUTHOR));
  Lines.Add(TABS[1] + BeginTag(TAG_DOCS) + EncodeXML(Comment) + EndTag(TAG_DOCS));
  Lines.Add(EndTag(TAG_PLUGIN_DESCRIPTOR));
end;

procedure TAHMPluginWrapper.ParseFromXML(XML: TAHMXMLHelper);
var
  RootNode, PropsNode: TAHMXMLNode;
begin
  // Initialise parsing nodes
  RootNode := XML.CurrentNode;
  PropsNode := nil;

  // Save our descriptor attributes
  Name := XML.GetAttribute(ATTR_NAME);
  Description := XML.GetAttribute(ATTR_DESC);

  // What nodes do we have here?
  while XML.NextElement(RootNode, PropsNode) do
  begin
    if XML.CurrentElement = TAG_PLUGIN then
      ParsePluginProperties(XML)
    else if XML.CurrentElement = TAG_VERSION then
      FVersion := XML.GetValue
    else if XML.CurrentElement = TAG_AUTHOR then
      FAuthor := XML.GetValue
    else if XML.CurrentElement = TAG_DOCS then
      Comment := XML.GetValue
    else
    begin
      LogError(XML_PRS_PLG_FOUND_UNKNOWN_CONTENT_TAG);
      State := osWarn;
    end;
  end;
end;

procedure TAHMPluginWrapper.BuildProperties(Properties: TAHMUIPropertyList);
begin
{$IFDEF DESIGNER}
  // Build plugin wrapper properties
  Properties.Add('Name',          'Name',         CAT_GENERAL, HINT_NAME);
  Properties.Add('Description',   'Description',  CAT_GENERAL, HINT_DESCRIPTION);
  Properties.Add('Documentation', 'Comment',      CAT_GENERAL, HINT_COMMENT);
  Properties.Add('Version',       'Version',      CAT_GENERAL, HINT_VERSION);
  Properties.Add('Author',        'Author',       CAT_GENERAL, HINT_AUTHOR);
  Properties.Add('Plugin Type',   'PluginType',   CAT_PLUGIN, HINT_PLUGINTYPE);
  Properties.Add('Internal Name', 'InternalName', CAT_PLUGIN, HINT_INTERNALNAME);
  Properties.Add('Path',          'Path',         CAT_PLUGIN, HINT_PATH);
  Properties.Add('Parameters',    'Parameters',   CAT_PLUGIN, HINT_PARAMS);
  Properties.Add('Window Name',   'WindowName',   CAT_PLUGIN, HINT_WINDOWNAME);
  Properties.Add('Window Class',  'WindowClass',  CAT_PLUGIN, HINT_WINDOWCLASS);
  Properties.Add('Window Locking', 'WindowLock',  CAT_PLUGIN, HINT_WINDOWLOCK);
{$ENDIF}
end;

procedure TAHMPluginWrapper.ParsePluginProperties(XML: TAHMXMLHelper);
var
  sType: String;
  PropsNode, PluginNode: TAHMXMLNode;
begin
  // Initialise parsing nodes
  PropsNode := XML.CurrentNode;
  PluginNode := nil;

  // Get plugin type attribute
  sType := XML.GetAttribute(ATTR_TYPE);
  if sType = ATTR_VAL_NATIVE then FPluginType := ptNative
  else if sType = ATTR_VAL_EXTERNAL then FPluginType := ptExternal;

  // Check all properties within plugin node
  while XML.NextElement(PropsNode, PluginNode) do
  begin
    // Common properties for native & external plugins
    if XML.CurrentElement = TAG_PATH then
      FPath := XML.GetValue
    // Custom properties for external plugins only
    else if FPluginType = ptExternal then
      if XML.CurrentElement = TAG_PARAMS then
        FParams := XML.GetValue
      else if XML.CurrentElement = TAG_WIN_CLASS then
        FWindowClass := XML.GetValue
      else if XML.CurrentElement = TAG_WIN_NAME then
        FWindowName := XML.GetValue
      else if XML.CurrentElement = TAG_WIN_LOCK then
        FWindowLock := Str2Bool(XML.GetValue);
  end;
end;

function TAHMPluginWrapper.ParseDescriptor: Boolean;
var
  XML: TAHMXMLHelper;
  RootNode: TAHMXMLNode;
begin
  // Set default result - use cached state to avoid repeat parsing
  Result := FValid;
  if FParsed then Exit else FParsed := True;

  // Clear current plugin properties
  ClearProperties;

  XML := TAHMXMLHelper.Create(ApplicationManagerInstance.PluginPath + FFilename);
  try
    // Ignore preamble - we're looking for a PluginDescriptor node
    if not XML.FindRootElement(RootNode, TAG_PLUGIN_DESCRIPTOR) then
    begin
      LogError(XML_PRS_PLG_MISSING_DESCRIPTOR_START_TAG);
      State := osError;
    end
    else
    begin
      // Use standard XML parsing method
      ParseFromXML(XML);

      // Check whether parsed plugin properties successfully
      Result := State = osValid;
      FValid := Result;
    end;
  finally
    XML.Free;
  end;
end;

procedure TAHMPluginWrapper.StoreDescriptor;
var
  Buffer: TStringList;
begin
  // Persist this descriptor to plugin folder
  Buffer := TStringList.Create;
  try
    Buffer.Add(XML_PREAMBLE);
    SaveAsXML(Buffer);
    Buffer.SaveToFile(ApplicationManagerInstance.PluginPath + FFileName);
  finally
    Buffer.Free;
  end;
end;

function TAHMPluginWrapper.Load: Boolean;
begin
  // Set default result - failed to load
  Result := False;

  // First we must parse our descriptor to determine plugin properties
  if not ParseDescriptor then Exit;

  // Create our plugin instance if necessary
  if not Assigned(FPlugin) then
    case FPluginType of
      ptNative:   FPlugin := TAHMNativePlugin.Create(Self);
      ptExternal: FPlugin := TAHMExternalPlugin.Create(Self);
      else        FPlugin := TAHMPlugin.Create(Self);
    end;

  if FPlugin.Load then
  begin
    Initialise;
    Result := True;
  end;
end;

procedure TAHMPluginWrapper.Initialise;
begin
  if Assigned(FPlugin) then FPlugin.Initialise;
end;

procedure TAHMPluginWrapper.Uninitialise;
begin
  if Assigned(FPlugin) then FPlugin.Uninitialise;
end;

procedure TAHMPluginWrapper.Hibernate;
begin
  if Assigned(FPlugin) then FPlugin.Hibernate;
end;

procedure TAHMPluginWrapper.Resume;
begin
  if Assigned(FPlugin) then FPlugin.Resume;
end;

function TAHMPluginWrapper.Unload: Boolean;
begin
  // Set default result - failed to unload
  Result := False;

  if Assigned(FPlugin) then
  begin
    // Remove any stale rendering references to this plugin before unloading
    HideFromAllControls;

    Result := FPlugin.Unload;
    FreeAndNil(FPlugin);
  end;
end;

function TAHMPluginWrapper.Start: Boolean;
begin
  // Only start a stopped plugin
  if Assigned(FPlugin) and not FPlugin.Started then Result := FPlugin.Start
                                               else Result := False;
end;

function TAHMPluginWrapper.Stop: Boolean;
begin
  // Only stop a started plugin
  if Assigned(FPlugin) and FPlugin.Started then Result := FPlugin.Stop
                                           else Result := False;
end;

function TAHMPluginWrapper.Update: Boolean;
begin
  if Assigned(FPlugin) then Result := FPlugin.Update
                       else Result := False;
end;

function TAHMPluginWrapper.Show(ControlName: String): Boolean;
var
  Control: TAHMControl;
begin
  // Lookup requested control
  Control := ApplicationManagerInstance.UIManager.GetControl(ControlName, False);
  if Control is TAHMPluginControl then
  begin
    // Associate control with our plugin
    TAHMPluginControl(Control).Plugin := FPlugin;
    Result := True;
  end
  else
    Result := False;
end;

function TAHMPluginWrapper.Hide(ControlName: String): Boolean;
var
  Control: TAHMControl;
begin
  // Lookup requested control
  Control := ApplicationManagerInstance.UIManager.GetControl(ControlName, False);
  if Control is TAHMPluginControl then
  begin
    // Remove association between control and plugin
    TAHMPluginControl(Control).Plugin := nil;
    Result := True;
  end
  else
    Result := False;
end;

procedure TAHMPluginWrapper.HideFromAllControls;
var
  i, j: Integer;
begin
  // Remove any stale control references pointing to this plugin
  with ApplicationManagerInstance.ThemeManager.InterfaceDescriptors do
    for i := 0 to Pred(Count) do
      with TAHMInterfaceDescriptor(Descriptor[i]) do
        for j := 0 to Pred(ControlList.TotalCount) do
          if ControlList.Objects[j] is TAHMPluginControl then
            if TAHMPluginControl(ControlList.Objects[j]).Plugin = FPlugin then
              TAHMPluginControl(ControlList.Objects[j]).Plugin := nil;
end;

procedure TAHMPluginWrapper.Reload;
begin
  // Reload our plugin - useful in case it was broken
  Unload;
  Load;
end;

function TAHMPluginWrapper.Configure: Boolean;
begin
  if Assigned(FPlugin) then Result := FPlugin.Configure
                       else Result := False;
end;

procedure TAHMPluginWrapper.Subscribe(ActionName: String);
begin
  if Assigned(FPlugin) then FPlugin.Subscribe(ActionName);
end;

procedure TAHMPluginWrapper.Unsubscribe(ActionName: String);
begin
  if Assigned(FPlugin) then FPlugin.Unsubscribe(ActionName);
end;

procedure TAHMPluginWrapper.ProcessAction(ActionName: String);
begin
  if Assigned(FPlugin) then FPlugin.ProcessAction(ActionName);
end;

procedure TAHMPluginWrapper.EmbedApplication(WindowName, WindowClass: String; WindowLock: Boolean);
begin
  if Assigned(FPlugin) then FPlugin.EmbedApplication(WindowName, WindowClass, WindowLock);
end;

procedure TAHMPluginWrapper.LaunchApplication(ExecutablePath, Parameters: String);
begin
  if Assigned(FPlugin) then FPlugin.LaunchApplication(ExecutablePath, Parameters);
end;

procedure TAHMPluginWrapper.CloseApplication(ExecutablePath: String);
begin
  if Assigned(FPlugin) then FPlugin.CloseApplication(ExecutablePath);
end;

procedure TAHMPluginWrapper.CloseAllApplications;
begin
 if Assigned(FPlugin) then FPlugin.CloseAllApplications;
end;

class function TAHMPluginWrapper.ClassName: ShortString;
begin
  Result := 'Plugin';
end;

// TAHMPlugin

constructor TAHMPlugin.Create(Owner: TAHMPluginWrapper);
begin
  inherited Create;

  // Store our owner wrapper
  FOwner := Owner;

  // Create and configure our subscription list
  FSubscriptions := TStringList.Create;
  FSubscriptions.Duplicates := dupIgnore;
  FSubscriptions.CaseSensitive := False;
  FSubscriptions.Sorted := True;

  // Create and configure our application list
  FApplications := TStringList.Create;
  FApplications.Duplicates := dupIgnore;
  FApplications.CaseSensitive := False;
  FApplications.Sorted := True;

  // Create RenderCache object
  FRenderCache := TAHMRenderCache.Create;
end;

destructor TAHMPlugin.Destroy;
begin
  Unload;

  // Free owned objects
  FSubscriptions.Free;
  FApplications.Free;
  FRenderCache.Free;

  inherited;
end;

function TAHMPlugin.GetLoaded: Boolean;
begin
  // Other plugins may override this
  Result := False;
end;

function TAHMPlugin.GetConfigurable: Boolean;
begin
  // Other plugins may override this
  Result := False;
end;

function TAHMPlugin.IsSubscribed(var ActionName: String): Boolean;
var
  iIndex: Integer;
begin
  // Check if we are subscribed to this action event
  iIndex := FSubscriptions.IndexOf(ActionName);
  Result := (iIndex >= 0);

  // Correct case of event name - use subscribed value
  if Result then ActionName := FSubscriptions[iIndex];
end;

function TAHMPlugin.Load: Boolean;
begin
  // Descendants will override this as necessary to implement loading
  Result := False;
end;

procedure TAHMPlugin.Initialise;
begin
  // Descendants will override this as necessary to implement initialisation
end;

procedure TAHMPlugin.Uninitialise;
begin
  // If we haven't yet stopped then autostop the plugin
  if Started then Stop;

  // Descendants will override this as necessary to implement uninitialisation
end;

procedure TAHMPlugin.Hibernate;
begin
  // Descendants will override this as necessary to implement hibernation
end;

procedure TAHMPlugin.Resume;
begin
  // Descendants will override this as necessary to implement resuming
end;

function TAHMPlugin.Unload: Boolean;
begin
  // Stop plugin first if currently started
  if FStarted then Stop;

  // Unsubscribe from all events & clear applications
  FSubscriptions.Clear;
  FApplications.Clear;

  // Descendants must override this method to support unloading
  Result := False;
end;

function TAHMPlugin.Start: Boolean;
begin
  // Flag plugin as started & schedule immediate render
  FStarted := True;
  FUpdatePriority := upImmediate;

  // Descendants must override this method to support starting
  Result := False;
end;

function TAHMPlugin.Stop: Boolean;
begin
  // Flag plugin as stopped
  FStarted := False;

  // Descendants must override this method to support stopping
  Result := False;
end;

function TAHMPlugin.Update: Boolean;
begin
  // Descendants must override this method to support updating
  FUpdatePriority := upImmediate;
  Result := False;
end;

function TAHMPlugin.Render: Boolean;
begin
  // If we haven't yet started then autostart the plugin
  if not Started then Start;

  // Is it time to render according to our priority?
  case FUpdatePriority of
    upImmediate: FRenderRequired := True;
    upNormal: FRenderRequired := TimerInstance.HasDurationElapsed(FRenderTimestamp, NORMAL_UPDATE_TIME);
    else FRenderRequired := TimerInstance.HasDurationElapsed(FRenderTimestamp, IDLE_UPDATE_TIME);
  end;

  // If render is required then reset timestamp for next render
  if FRenderRequired then
  begin
    FRenderTimestamp := TimerInstance.CurrentTimeStamp;
    FUpdatePriority := upIdle;
  end;

  // Descendants must override this method to support rendering
  Result := False;
end;

function TAHMPlugin.Configure: Boolean;
begin
  // Descendants will override this as necessary to implement configuration
  Result := False;
end;

procedure TAHMPlugin.Subscribe(ActionName: String);
begin
  // Add subscribed event to our subscription list
  FSubscriptions.Add(ActionName);
end;

procedure TAHMPlugin.UnSubscribe(ActionName: String);
var
  i: Integer;
begin
  // Remove subscribed event from our subscription list
  i := FSubscriptions.IndexOf(ActionName);
  if i >= 0 then FSubscriptions.Delete(i);
end;

procedure TAHMPlugin.ProcessAction(ActionName: String);
begin
  // Descendants must override this method to support event processing
  if FUpdatePriority <> upImmediate then FUpdatePriority := upNormal;
end;

procedure TAHMPlugin.EmbedApplication(WindowName, WindowClass: String; WindowLock: Boolean);
begin
  // Attach to specified application window
  FEmbed := ApplicationManagerInstance.PluginManager.EmbeddingManager.AttachApplication(WindowName, WindowClass, WindowLock);
  if FUpdatePriority <> upImmediate then FUpdatePriority := upNormal;
end;

procedure TAHMPlugin.LaunchApplication(ExecutablePath, Parameters: String);
var
  PID, Index: Integer;
begin
  // Run appropriate process and get its Id
  PID := RunProcess(ExecutablePath, Parameters, True);

  // If successful then add to our list of known applications
  if PID > 0 then
  begin
    Index := FApplications.Add(ExecutablePath);
    FApplications.Objects[Index] := TObject(PID);

    // Bring mediate back to front
    ScreenInstance.BringToFront;
  end;
end;

procedure TAHMPlugin.CloseApplication(ExecutablePath: String);
var
  Index, PID: Integer;
begin
  // Lookup requested application in our list
  Index := FApplications.IndexOf(ExecutablePath);

  // If found then delete appropriate process and remove listing
  if Index >= 0 then
  begin
    PID := Integer(FApplications.Objects[Index]);
    KillProcess(PID);
    FApplications.Delete(Index);
  end;
end;

procedure TAHMPlugin.CloseAllApplications;
var
  i, PID: Integer;
begin
  // Close all applications we previously opened
  for i := Pred(FApplications.Count) downto 0 do
  begin
    PID := Integer(FApplications.Objects[i]);
    KillProcess(PID);
  end;

  FApplications.Clear;
end;

procedure TAHMPlugin.MouseDown(X, Y: Single);
begin
  // Pass request to our embedded application instance if assigned
  if FUpdatePriority <> upImmediate then FUpdatePriority := upNormal;
  if Assigned(FEmbed) then FEmbed.MouseDown(X, Y);
end;

procedure TAHMPlugin.MouseUp(X, Y: Single);
begin
  // Pass request to our embedded application instance if assigned
  if FUpdatePriority <> upImmediate then FUpdatePriority := upNormal;
  if Assigned(FEmbed) then FEmbed.MouseUp(X, Y);
end;

procedure TAHMPlugin.MouseMove(X, Y: Single; Down: Boolean);
begin
  // Pass request to our embedded application instance if assigned
  if FUpdatePriority <> upImmediate then FUpdatePriority := upNormal;
  if Assigned(FEmbed) then FEmbed.MouseMove(X, Y, Down);
end;

procedure TAHMPlugin.KeyPress(KeyCode: String);
var
  sKey: String;
  Code: Word;
  Shift: TShiftState;
begin
  // Set default shift state
  Shift := [];

  // Check for known virtual key names
  sKey := LowerCase(KeyCode);
  if sKey = KEY_VAL_ENTER then Code := VK_RETURN
  else if sKey = KEY_VAL_BACKSPACE then Code := VK_BACK
  else if sKey = KEY_VAL_DELETE then Code := VK_DELETE
  else if sKey = KEY_VAL_LEFT then Code := VK_LEFT
  else if sKey = KEY_VAL_RIGHT then Code := VK_RIGHT
  else if sKey = KEY_VAL_UP then Code := VK_UP
  else if sKey = KEY_VAL_DOWN then Code := VK_DOWN
  else if sKey = KEY_VAL_HOME then Code := VK_HOME
  else if sKey = KEY_VAL_END then Code := VK_END
  else if sKey = KEY_VAL_PAGEUP then Code := VK_PRIOR
  else if sKey = KEY_VAL_PAGEDN then Code := VK_NEXT
  else if sKey = KEY_VAL_TAB then Code := VK_TAB
  else if sKey = KEY_VAL_SPACE then Code := VK_SPACE
  else if sKey <> '' then
  begin
    // Map characters to unshifted keycode equivalents
    case Ord(sKey[1]) of
      33..37: Code := Ord(sKey[1]) + 16;
      38: Code := 55;
      39, 64: Code := 192;
      40: Code := 57;
      41: Code := 48;
      42: Code := 56;
      43, 61: Code := 187;
      44, 60: Code := 188;
      45, 95: Code := 189;
      46, 62: Code := 190;
      47, 63: Code := 191;
      58, 59: Code := 186;
      91, 123: Code := 219;
      92, 124: Code := 220;
      93, 125: Code := 221;
      94: Code := 54;
      97..122: Code := Ord(sKey[1]) - 32;
      126: Code := 222;
      else Code := Ord(sKey[1]);
    end;

    // Check for shifted characters
    if Ord(sKey[1]) in [33..38, 40..43, 58, 60, 62..90, 94..95, 123..126] then
      Include(Shift, ssShift);
  end
  else
    Exit;

  // Pass request to our embedded application instance if assigned
  if FUpdatePriority <> upImmediate then FUpdatePriority := upNormal;
  if Assigned(FEmbed) then FEmbed.KeyPress(Code, Shift);
end;

procedure TAHMPlugin.KeyPress(KeyCode: Word; Shift: TShiftState);
begin
  // Pass request to our embedded application instance if assigned
  if FUpdatePriority <> upImmediate then FUpdatePriority := upNormal;
  if Assigned(FEmbed) then FEmbed.KeyPress(KeyCode, Shift);
end;

// TAHMNativePlugin

function TAHMNativePlugin.GetLoaded: Boolean;
begin
  Result := (FDLLHandle <> 0);
end;

function TAHMNativePlugin.GetConfigurable: Boolean;
begin
  Result := True;
end;

procedure TAHMNativePlugin.LoadPluginProperties;
var
  Tx: WideString;
begin
  // Call standard methods on plugin to get version info
  try
    if FPlugin.GetName(Tx) = S_OK then FOwner.Name := Tx;
    if FPlugin.GetDescription(Tx) = S_OK then FOwner.Description := Tx;
    if FPlugin.GetVersion(Tx) = S_OK then FOwner.Version := Tx;
    if FPlugin.GetAuthor(Tx) = S_OK then FOwner.Author := Tx;
    if FPlugin.GetDocumentation(Tx) = S_OK then FOwner.Comment := Tx;
  except
    on E: Exception do
      LogWarn('Failed to identify native plugin ' + FOwner.InternalName + ': ' + E.Message);
  end;
end;

function TAHMNativePlugin.Load: Boolean;
const
  IID_IClassFactory: TGUID = '{00000001-0000-0000-C000-000000000046}';
var
  ClassMethod: TDLLGetClassObject;
  Factory: IClassFactory;
  Filename: String;
begin
  // Don't attempt to load DLL if already loaded
  Result := GetLoaded;
  if Result then Exit;

  try
    // Unload any current instance of plugin
    Unload;

    // Attempt to load the plugin dll
    Filename := ApplicationManagerInstance.BinaryPath + FOwner.Path;
    FDLLHandle := LoadLibrary(PChar(Filename));
    if FDLLHandle = 0 then
      raise EPluginException.Create('Failed to open plugin library: ' + FileName);

    // Get process address of GetClassObject method
    @ClassMethod := GetProcAddress(FDLLHandle, 'DllGetClassObject');
    if @ClassMethod = nil then
      raise EPluginException.Create('Plugin ' + Filename + ' does not appear to be a valid COM library');

    // Get the class factory interface
    if ClassMethod(CLASS_MediatePlugin, IID_IClassFactory, Factory) <> S_OK then
      raise EPluginException.Create('Plugin ' + Filename + ' is not a valid mediate plugin');

    // Now use the class factory to create a plugin instance
    if Factory.CreateInstance(nil, IID_IMediatePlugin, FPlugin) <> S_OK then
      raise EPluginException.Create('Plugin ' + Filename + ' instance creation failed');

    // Query plugin for its standard properties
    LoadPluginProperties;
    Result := True;
  except
    on E:Exception do
      LogWarn('Failed to load plugin ' + FOwner.InternalName + ': ' + E.Message);
  end;
end;

procedure TAHMNativePlugin.Initialise;
begin
  // Check plugin is loaded then signal initialisation
  if FPlugin <> nil then
  begin
    // Create private MediateServer instance on first initialise
    if not Assigned(FServer) then
    begin
      FServer := TMediateServer.Create;
      FServer.InternalName := FOwner.InternalName;
    end;

    try
      FPlugin.OnLoad(FServer as IMediateServer);
    except
      on E:Exception do
        LogError('Failed to initialise native plugin ' + FOwner.InternalName + ': ' + E.Message);
    end;
  end;
end;

procedure TAHMNativePlugin.Uninitialise;
begin
  // Handle standard autostop processing
  inherited;

  // Check plugin is loaded then signal uninitialisation
  if FPlugin <> nil then
    try
      FPlugin.OnUnLoad;
    except
      on E: Exception do
        LogWarn('Failed to unload native plugin ' + FOwner.InternalName + ': ' + E.Message);
    end;
end;

procedure TAHMNativePlugin.Hibernate;
begin
  // Check plugin is loaded then signal hibernation
  if FPlugin <> nil then
    try
      FPlugin.OnSleep;
    except
      on E: Exception do
        LogWarn('Failed to hibernate native plugin ' + FOwner.InternalName + ': ' + E.Message);
    end;
end;

procedure TAHMNativePlugin.Resume;
begin
  // Check plugin is loaded then signal resume from hibernate
  if FPlugin <> nil then
    try
      FPlugin.OnWake;
    except
      on E: Exception do
        LogWarn('Failed to resume native plugin ' + FOwner.InternalName + ': ' + E.Message);
    end;
end;

function TAHMNativePlugin.Configure: Boolean;
begin
  Result := inherited Configure;

  // Check plugin is loaded then signal configuration
  if FPlugin <> nil then
    try
      Result := (FPlugin.OnConfigure <> E_NOTIMPL);
    except
      on E: Exception do
        LogWarn('Failed to configure native plugin ' + FOwner.InternalName + ': ' + E.Message);
    end;
end;

function TAHMNativePlugin.Unload: Boolean;
begin
  // Call inherited method to clear standard properties
  inherited Unload;

  // Free plugin & server COM instance
  FPlugin := nil;
  FServer := nil;

  // Close DLL handle
  if FDLLHandle <> 0 then FreeLibrary(FDLLHandle);
  FDLLHandle := 0;

  Result := True;
end;

function TAHMNativePlugin.Start: Boolean;
begin
  Result := inherited Start;

  // Check plugin is loaded then signal start
  if FPlugin <> nil then
    try
      FPlugin.OnInitialise;

      // Assume plugin supports rendering event (it may fail later)
      FRenderImplemented := True;

      Result := True;
    except
      on E: Exception do
        LogWarn('Failed to start native plugin ' + FOwner.InternalName + ': ' + E.Message);
    end;
end;

function TAHMNativePlugin.Stop: Boolean;
begin
  Result := inherited Stop;

  // Check plugin is loaded then signal stop
  if FPlugin <> nil then
    try
      FPlugin.OnFinalise;

      Result := True;
    except
      on E: Exception do
        LogWarn('Failed to stop native plugin ' + FOwner.InternalName + ': ' + E.Message);
    end;
end;

function TAHMNativePlugin.Update: Boolean;
begin
  Result := inherited Update;

  // Check plugin is loaded then signal update
  if FPlugin <> nil then
    try
      FPlugin.OnUpdate;
      Result := True;
    except
      on E: Exception do
        LogWarn('Failed to update native plugin ' + FOwner.InternalName + ': ' + E.Message);
    end;
end;

function TAHMNativePlugin.Render: Boolean;
begin
  Result := inherited Render;

  // Only render when required - improves performance
  if RenderRequired then
  begin
    // If we have an embedded application assigned use this for rendering
    if Assigned(FEmbed) then
    begin
      FEmbed.UpdateDisplayCache(FRenderCache);
      Result := True;
    end;

    // Allow plugin to perform additional rendering if implemented
    if FRenderImplemented and (FPlugin <> nil) then
      try
        // Check result of render method - E_NOTIMPL means we should stop calling
        if FPlugin.OnRender = E_NOTIMPL then
          FRenderImplemented := False
        else
          Result := True;
      except
        on E: Exception do
          LogWarn('Failed to render native plugin ' + FOwner.InternalName + ': ' + E.Message);
      end;
  end;
end;

procedure TAHMNativePlugin.ProcessAction(ActionName: String);
begin
  inherited;

  // Check plugin is loaded and subscribed then signal event
  if (FPlugin <> nil) and IsSubscribed(ActionName) then
    FPlugin.OnEvent(ActionName);
end;

// TAHMExternalPlugin

function TAHMExternalPlugin.GetLoaded: Boolean;
begin
  Result := FLoaded;
end;

function TAHMExternalPlugin.GetConfigurable: Boolean;
begin
//TODO For now... we may add a configuration dialog later
  Result := False;
end;

function TAHMExternalPlugin.Load: Boolean;
begin
  // We don't need to load anything, so just simulate success
  Result := True;
  FLoaded := True;
end;

function TAHMExternalPlugin.Configure: Boolean;
begin
 //TODO: we don't support external plugin configuration yet
  Result := False;
end;

function TAHMExternalPlugin.Unload: Boolean;
begin
  inherited Unload;

  // Nothing to do, so just clear our loaded flag
  FLoaded := False;
  Result := True;
end;

function TAHMExternalPlugin.Start: Boolean;
begin
  inherited Start;

  // Launch our associated application if not yet running
  if FOwner.Path <> '' then
    if not ProcessRunning(FOwner.Path) then
      LaunchApplication(FOwner.Path, FOwner.Parameters);

  // Embed application if required
  if (FOwner.WindowName <> '') or (FOwner.WindowClass <> '') then
    EmbedApplication(FOwner.WindowName, FOwner.WindowClass, FOwner.WindowLock);

  Result := True;
end;

function TAHMExternalPlugin.Stop: Boolean;
begin
  inherited Stop;

  // Stop associated application if launched previously
  if FOwner.Path <> '' then
    CloseApplication(FOwner.Path);

  Result := True;
end;

function TAHMExternalPlugin.Render: Boolean;
begin
  Result := inherited Render;

  // Only render when required - improves performance
  if RenderRequired and Assigned(FEmbed) then
  begin
    FEmbed.UpdateDisplayCache(FRenderCache);
    Result := True;
  end;
end;

// TAHMPlugins

constructor TAHMPlugins.Create;
begin
  inherited;

  FPlugins := TList.Create;
end;

destructor TAHMPlugins.Destroy;
begin
  FPlugins.Free;

  inherited;
end;

function TAHMPlugins.GetCount: Integer;
begin
  Result := FPlugins.Count;
end;

function TAHMPlugins.GetPlugin(Index: Integer): TAHMPluginWrapper;
begin
  if (Index >= 0) and (Index < FPlugins.Count) then
    Result := TAHMPluginWrapper(FPlugins.Items[Index])
  else
    Result := nil;
end;

procedure TAHMPlugins.Clear;
var
  i: Integer;
begin
  for i := 0 to Pred(FPlugins.Count) do
  begin
    TAHMPluginWrapper(FPlugins.Items[i]).Uninitialise;
    TAHMPluginWrapper(FPlugins.Items[i]).Free;
  end;

  FPlugins.Clear;
end;

function TAHMPlugins.AddPlugin(Filename: String): TAHMPluginWrapper;
begin
  Result := TAHMPluginWrapper.Create('');
  Result.Filename := Filename;

//TODO - check enabled status in PreferencesManager (+ persist)
  Result.Enabled := True;

  FPlugins.Add(Result);
end;

function TAHMPlugins.FindPlugin(InternalName: String): TAHMPluginWrapper;
var
  i: Integer;
  sComparison: String;
begin
  // Set default result - plugin not found
  Result := nil;
  sComparison := UpperCase(InternalName);

  // Iterate through all known plugins
  for i := 0 to Pred(FPlugins.Count) do
    if TAHMPluginWrapper(FPlugins.Items[i]).InternalName = sComparison then
    begin
      Result := TAHMPluginWrapper(FPlugins.Items[i]);
      Break;
    end;
end;

// TAHMPluginManager

constructor TAHMPluginManager.Create;
begin
  inherited;

  FPlugins := TAHMPlugins.Create;
  FEmbedManager := TAHMEmbeddingManager.Create;
end;

destructor TAHMPluginManager.Destroy;
begin
  FPlugins.Clear;
  FPlugins.Free;
  FEmbedManager.Free;

  inherited;
end;

procedure TAHMPluginManager.LoadPluginDescriptors;
var
  PluginDir: String;
  Found, Attr: Integer;
  SearchRec: TSearchRec;
begin
  // Unload all current plugins
  UnloadPlugins;

  // Enumerate all plugin descriptors in our plugin folder
  PluginDir := ApplicationManagerInstance.PluginPath;

  Attr := faArchive or faReadOnly;
  Found := FindFirst(PluginDir + '*' + EXT_PLUGIN, Attr, SearchRec);
  while Found = 0 do
  begin
    // Attempt to load each plugin we find
    FPlugins.AddPlugin(SearchRec.Name);
    Found := FindNext(SearchRec);
  end;
  FindClose(SearchRec);
end;

procedure TAHMPluginManager.SavePluginDescriptors;
var
  i: Integer;
begin
  // Any modified plugin descriptors will be saved
  for i := 0 to Pred(FPlugins.Count) do
    if FPlugins[i].Modified then
    begin
      FPlugins[i].StoreDescriptor;
      FPlugins[i].Modified := False;
    end;
end;

function TAHMPluginManager.InspectPlugin(PluginName: String): TAHMPluginWrapper;
var
  Plugin: TAHMPluginWrapper;
begin
  // Set default result - failed to load
  Result := nil;

  // See if plugin is already known
  Plugin := FPlugins.FindPlugin(PluginName);

  // Load plugin if not already loaded
  if Assigned(Plugin) and Plugin.Loaded or Plugin.Load then
    Result := Plugin;
end;

function TAHMPluginManager.LoadPlugin(PluginName: String): Boolean;
var
  Plugin: TAHMPluginWrapper;
begin
  // Set default result - failed to load
  Result := False;

  // See if plugin is already known
  Plugin := FPlugins.FindPlugin(PluginName);

  // Load plugin if not already loaded
  if not Assigned(Plugin) then
    LogDebug('Unknown plugin requested for loading: ' + PluginName)
  else if Plugin.Loaded then
    LogDebug('Requested load of already loaded plugin: ' + PluginName)
  else if not Plugin.Load then
    LogWarn('Failed to load requested plugin: ' + PluginName)
  else
    Result := True;
end;

function TAHMPluginManager.UnloadPlugin(PluginName: String): Boolean;
var
  Plugin: TAHMPluginWrapper;
begin
  // Set default result - failed to unload
  Result := False;

  // See if plugin is already known
  Plugin := FPlugins.FindPlugin(PluginName);

  // Unload plugin if already loaded
  if not Assigned(Plugin) then
    LogDebug('Unknown plugin requested for unloading: ' + PluginName)
  else if not Plugin.Loaded then
    LogDebug('Requested unload of already unloaded plugin: ' + PluginName)
  else if not Plugin.Unload then
    LogWarn('Failed to unload requested plugin: ' + PluginName)
  else
    Result := True;
end;

function TAHMPluginManager.StartPlugin(PluginName: String): Boolean;
var
  Plugin: TAHMPluginWrapper;
begin
  // Set default result - failed to start
  Result := False;

  // See if plugin is already known
  Plugin := FPlugins.FindPlugin(PluginName);

  // Start plugin if already loaded
  if not Assigned(Plugin) then
    LogDebug('Unknown plugin requested for starting: ' + PluginName)
  else if not Plugin.Loaded then
    LogDebug('Requested start of unloaded plugin: ' + PluginName)
  else if not Plugin.Start then
    LogWarn('Failed to start requested plugin: ' + PluginName)
  else
    Result := True;
end;

function TAHMPluginManager.StopPlugin(PluginName: String): Boolean;
var
  Plugin: TAHMPluginWrapper;
begin
  // Set default result - failed to start
  Result := False;

  // See if plugin is already known
  Plugin := FPlugins.FindPlugin(PluginName);

  // Stop plugin if already loaded
  if not Assigned(Plugin) then
    LogDebug('Unknown plugin requested for stopping: ' + PluginName)
  else if not Plugin.Loaded then
    LogDebug('Requested stop of unloaded plugin: ' + PluginName)
  else if not Plugin.Stop then
    LogWarn('Failed to stop requested plugin: ' + PluginName)
  else
  begin
    // Remove any stale rendering references to this plugin
    Plugin.HideFromAllControls;
    Result := True;
  end;  
end;

function TAHMPluginManager.UpdatePlugin(PluginName: String): Boolean;
var
  Plugin: TAHMPluginWrapper;
begin
  // Set default result - failed to update
  Result := False;

  // See if plugin is already known
  Plugin := FPlugins.FindPlugin(PluginName);

  // Update plugin if loaded
  if not Assigned(Plugin) then
    LogDebug('Unknown plugin requested for updating: ' + PluginName)
  else if not Plugin.Loaded then
    LogDebug('Requested update of unloaded plugin: ' + PluginName)
  else if not Plugin.Update then
    LogWarn('Failed to update requested plugin: ' + PluginName)
  else
    Result := True;
end;

function TAHMPluginManager.ShowPlugin(PluginName, ControlName: String): Boolean;
var
  Plugin: TAHMPluginWrapper;
begin
  // Set default result - failed to show plugin
  Result := False;

  // See if plugin is already known
  Plugin := FPlugins.FindPlugin(PluginName);

  // Show plugin if loaded
  if not Assigned(Plugin) then
    LogDebug('Unknown plugin requested for showing: ' + PluginName)
  else if not Plugin.Loaded then
    LogDebug('Requested show of unloaded plugin: ' + PluginName)
  else if not Plugin.Show(ControlName) then
    LogWarn('Failed to show requested plugin: ' + PluginName)
  else
    Result := True;
end;

function TAHMPluginManager.HidePlugin(PluginName, ControlName: String): Boolean;
var
  Plugin: TAHMPluginWrapper;
begin
  // Set default result - failed to hide plugin
  Result := False;

  // See if plugin is already known
  Plugin := FPlugins.FindPlugin(PluginName);

  // Show plugin if loaded
  if not Assigned(Plugin) then
    LogDebug('Unknown plugin requested for hiding: ' + PluginName)
  else if not Plugin.Loaded then
    LogDebug('Requested hide of unloaded plugin: ' + PluginName)
  else if not Plugin.Hide(ControlName) then
    LogWarn('Failed to hide requested plugin: ' + PluginName)
  else
    Result := True;
end;

function TAHMPluginManager.SubscribePlugin(PluginName, ActionName: String): Boolean;
var
  Plugin: TAHMPluginWrapper;
begin
  // Set default result - failed to subscribe
  Result := False;

  // See if plugin is already known
  Plugin := FPlugins.FindPlugin(PluginName);

  // Subscribe to action if plugin is loaded
  if not Assigned(Plugin) then
    LogDebug('Unknown plugin requested for subscription: ' + PluginName)
  else if not Plugin.Loaded then
    LogDebug('Requested subscription of unloaded plugin: ' + PluginName)
  else
  begin
    Plugin.Subscribe(ActionName);
    Result := True;
  end;
end;

function TAHMPluginManager.UnSubscribePlugin(PluginName, ActionName: String): Boolean;
var
  Plugin: TAHMPluginWrapper;
begin
  // Set default result - failed to unsubscribe
  Result := False;

  // See if plugin is already known
  Plugin := FPlugins.FindPlugin(PluginName);

  // UnSubscribe from action if plugin is loaded
  if not Assigned(Plugin) then
    LogDebug('Unknown plugin requested for unsubscription: ' + PluginName)
  else if not Plugin.Loaded then
    LogDebug('Requested unsubscription of unloaded plugin: ' + PluginName)
  else
  begin
    Plugin.UnSubscribe(ActionName);
    Result := True;
  end;
end;

function TAHMPluginManager.EmbedPluginApplication(PluginName, WindowName, WindowClass: String; WindowLock: Boolean): Boolean;
var
  Plugin: TAHMPluginWrapper;
begin
  // Set default result - failed to launch application
  Result := False;

  // See if plugin is already known
  Plugin := FPlugins.FindPlugin(PluginName);

  // Embed application if plugin is loaded
  if not Assigned(Plugin) then
    LogDebug('Unknown plugin requested application embedding: ' + PluginName)
  else if not Plugin.Loaded then
    LogDebug('Requested application embedding for unloaded plugin: ' + PluginName)
  else
  begin
    Plugin.EmbedApplication(WindowName, WindowClass, WindowLock);
    Result := True;
  end;
end;

function TAHMPluginManager.LaunchPluginApplication(PluginName, ExecutablePath, Parameters: String): Boolean;
var
  Plugin: TAHMPluginWrapper;
begin
  // Set default result - failed to launch application
  Result := False;

  // See if plugin is already known
  Plugin := FPlugins.FindPlugin(PluginName);

  // Launch application if plugin is loaded
  if not Assigned(Plugin) then
    LogDebug('Unknown plugin requested application launch: ' + PluginName)
  else if not Plugin.Loaded then
    LogDebug('Requested launch of application for unloaded plugin: ' + PluginName)
  else
  begin
    Plugin.LaunchApplication(ExecutablePath, Parameters);
    Result := True;
  end;
end;

function TAHMPluginManager.ClosePluginApplication(PluginName, ExecutablePath: String): Boolean;
var
  Plugin: TAHMPluginWrapper;
begin
  // Set default result - failed to close application
  Result := False;

  // See if plugin is already known
  Plugin := FPlugins.FindPlugin(PluginName);

  // Launch application if plugin is loaded
  if not Assigned(Plugin) then
    LogDebug('Unknown plugin requested application close: ' + PluginName)
  else if not Plugin.Loaded then
    LogDebug('Requested close of application for unloaded plugin: ' + PluginName)
  else
  begin
    Plugin.CloseApplication(ExecutablePath);
    Result := True;
  end;
end;

function TAHMPluginManager.CloseAllPluginApplications(PluginName: String): Boolean;
var
  Plugin: TAHMPluginWrapper;
begin
  // Set default result - failed to launch application
  Result := False;

  // See if plugin is already known
  Plugin := FPlugins.FindPlugin(PluginName);

  // Launch application if plugin is loaded
  if not Assigned(Plugin) then
    LogDebug('Unknown plugin requested closing all applications: ' + PluginName)
  else if not Plugin.Loaded then
    LogDebug('Requested close of all applications for unloaded plugin: ' + PluginName)
  else
  begin
    Plugin.CloseAllApplications;
    Result := True;
  end;
end;

function TAHMPluginManager.BrowsePluginFile(var Filename: String; PluginType: TAHMPluginType): Boolean;
{$IFDEF DESIGNER}
var
  Work, PluginPath, Filter: String;
{$ENDIF}
begin
{$IFDEF DESIGNER}
  // Browse files in plugin directory
  Work := Trim(Filename);
  case PluginType of
    ptNative: Filter := FILTER_NATIVE;
    ptExternal: Filter := FILTER_EXTERNAL;
    else Filter := FILTER_ALLFILES;
  end;
  PluginPath := ApplicationManagerInstance.BinaryPath;
  Result := ChooseFile(BROWSE_FILE_TITLE, Filter, '', PluginPath, Work, False);

  if Result then
  begin
    Work := Trim(Work);

    // Check whether filename points to the plugin directory
    if SameText(Copy(Work, 1, Length(PluginPath)), PluginPath) then
    begin
      // Remove plugin directory prefix from filename
      Filename := Copy(Work, Length(PluginPath) + 1, Length(Work));
    end
    else if PluginType = ptExternal then
    begin
      // Return full path to external application
      Filename := Work;
    end
    else
    begin
      // Prompt to copy native plugin from another location
      if MessageDlg(COPY_FILE_CONFIRM, mtConfirmation, [mbYes, mbNo], 0) = mrYes then
      begin
        Filename := ExtractFileName(Work);
        FileCopy(Work, PluginPath + Filename);
      end
      else
        Result := False;
    end;
  end;
{$ELSE}
  Result := False;
{$ENDIF}
end;

procedure TAHMPluginManager.ProcessAction(ActionName: String);
var
  i: Integer;
begin
  // Iterate through all plugins and allow them to handle this action
  for i := 0 to Pred(FPlugins.Count) do
    FPlugins.GetPlugin(i).ProcessAction(ActionName);
end;

procedure TAHMPluginManager.HibernatePlugins;
var
  i: Integer;
begin
  // Iterate through all plugins and allow them to hibernate if required
  for i := 0 to Pred(FPlugins.Count) do
    FPlugins.GetPlugin(i).Hibernate;
end;

procedure TAHMPluginManager.ResumePlugins;
var
  i: Integer;
begin
  // Iterate through all plugins and allow them to resume from hibernation
  for i := 0 to Pred(FPlugins.Count) do
    FPlugins.GetPlugin(i).Resume;
end;

procedure TAHMPluginManager.UnloadPlugins;
begin
  // Unload all current plugins
  FPlugins.Clear;
end;



end.
